# -*- coding:utf-8 -*-

import requests
import json
import time
import base64
from Crypto.Cipher import AES
import execjs # 调用JS
from pyquery import PyQuery
from sys import stdin # 控制台输入
import re # 正则匹配
import os
from contextlib import closing # 持续下载响应内容


class Music_API():

    # 添加头信息。模拟浏览器(成员变量)
    base_header = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
        # 'Upgrade-Insecure-Requests': '1'
        'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
        # 加密接口头
        # 'Cookie': 'appver=1.5.0.75771;',
        'Content-Type': 'application/x-www-form-urlencoded',
        'Host': 'music.163.com',
        'Referer': 'http://music.163.com/',
        'Connection': 'close'
    }

    # 获取歌曲评论信息 接口参数
    # url1 = "http://music.163.com/weapi/v1/resource/comments/R_SO_4_432506345/?csrf_token="
    # first_param1 = "{rid:\"\", offset:\"0\", total:\"true\", limit:\"20\", csrf_token:\"\"}"

    # 遍历全部评论 by 歌曲id 页码 每页显示数 是否显示热评
    def getCommentsBySongId(self, songId, pageNum, count, flag=False):
        if pageNum < 1:
            pageNum = 0
        else:
            pageNum = (pageNum - 1) * count

        # 查看歌曲评论api
        res = self.__sendGet("http://music.163.com/api/v1/resource/comments/R_SO_4_{}?offset={}&limit={}".format(songId, pageNum, count))
        if res:
            json_res = json.loads(res)
            commentsKey = "hotComments" if flag else "comments"  # 热门评论 | 普通评论
            count = 0
            for item in json_res[commentsKey]:
                user = item['user']
                print("{} - 用户：{}【{}】- 点赞：{} - 评论：{}".format(self.getTime(item['time'] / 1000), user['nickname'],
                                                             user['userId'], item['likedCount'], item['content']))
                # 遍历回复
                if len(item['beReplied']) > 0:
                    for rep in item['beReplied']:
                        print("-- 回复了：{} - 的评论：{}".format(rep['user']['nickname'], rep['content']))
                count += 1
                print("")
            print("----- 共计【{}】条评论 -----".format(count))
            return json_res
        else:
            print("---- 接口请求异常 ----")
            return None

    # 根据用户id获取其近期听过热度前100首歌曲的信息
    # url = "https://music.163.com/weapi/v1/play/record?csrf_token="
    # 参数：{"uid":"1234","type":"-1","limit":"1000","offset":"0","total":"true","csrf_token":""}
    def getSongsByUser(self, uid):
        self.base_header['Referer'] = "https://music.163.com/user/home?id=%s" % uid

        paramJson = "{\"uid\":\"" + uid + "\",\"type\":\"-1\",\"limit\":\"1000\",\"offset\":\"0\",\"total\":\"true\",\"csrf_token\":\"\"}"
        params, encSecKey = self.__getParamsAndEncSecKey(paramJson)

        res = self.__sendPost("https://music.163.com/weapi/v1/play/record?csrf_token=", params, encSecKey, self.base_header)
        if res:
            responseJson = json.loads(res)
            if responseJson['code'] == 200:
                for item in responseJson['allData']:
                    song = item['song']
                    msg = "id:{}\t热度：{}%\t  -  歌曲名称：{}".format(song['id'], item['score'], song['name'])
                    for singer in song['ar']:
                        msg += "  -  歌手：{}".format(singer['name'])
                    print(msg)
            else:
                print("接口返回异常！code : {}, msg : {}".format(responseJson['code'], res))
        else:
            print("接口挂了！！！")

    # 根据用户id获取创建的歌单
    # url = "https://music.163.com/weapi/user/playlist?csrf_token="
    # 参数：{"uid":"1234","wordwrap":"7","offset":"0","total":"true","limit":"36","csrf_token":""}
    # 通过 playlist[creator.userId] 来判断该歌单的创建者是本人还是收藏得到的歌单
    def getPlayListByUser(self, uid):
        paramJson = "{\"uid\":\"" + uid + "\",\"wordwrap\":\"7\",\"offset\":\"0\",\"total\":\"true\",\"limit\":\"36\",\"csrf_token\":\"\"}"
        params, encSecKey = self.__getParamsAndEncSecKey(paramJson)

        res = self.__sendPost("https://music.163.com/weapi/user/playlist?csrf_token=", params, encSecKey)
        if res:
            responseJson = json.loads(res)
            if responseJson['code'] == 200:
                for item in responseJson['playlist']:
                    creator = item['creator']  # 创建人信息
                    playListType = "创建歌单" if creator['userId'] == int(uid) else "收藏歌单"
                    msg = "【{}】名称：{} - 播放次数【{}】- id:{} -\t创建时间：{} ".format(playListType, item['name'],
                                                                           item['playCount'], item['id'],
                                                                           self.getTime(item['createTime'] / 1000))
                    print(msg)
            else:
                print("接口返回异常！code : {}, msg : {}".format(responseJson['code'], responseJson))
        else:
            print("接口挂了！！！")

    # 获取用户主页上的信息
    def getUserInfo(self, uid):
        self.base_header['Content-Type'] = "text/html;charset=utf8"
        self.base_header['Referer'] = "https://music.163.com/"
        res = self.__sendGet("https://music.163.com/user/home?id={}".format(uid), self.base_header)
        if res:
            # print("responseStr = %s" % res.decode())
            # 解析
            html = PyQuery(res.decode())
            user_name = html("#j-name-wrap .tit").text()
            user_level = html("#j-name-wrap .lev").text()
            user_address = html(".inf span:first-child").text()
            user_age = html("#age").attr("data-age")
            event_count = html("#event_count").text()  # 动态
            follow_count = html("#follow_count").text()  # 关注
            fan_count = html("#fan_count").text()  # 粉丝

            # .u-icn-01 是男性图标 .u-icn-02 是女性图标
            sex_man = html("#j-name-wrap .u-icn-01")
            sex_woman = html("#j-name-wrap .u-icn-02")
            sex_icn = ""
            if sex_man:
                sex_icn = "【boy】"
            if sex_woman:
                sex_icn = "【girl】"

            print("用户id:{}，名称：{}{}，{}级，年龄：{} 地区【{}】-- 动态【{}】关注【{}】粉丝【{}】".format(uid, user_name, sex_icn, user_level,
                                                                                    self.getTime(int( user_age) / 1000) if user_age != None else "",
                                                                                    user_address, event_count, follow_count, fan_count))
            return ""
        else:
            print("---- 接口请求异常 ----")
            return None

    # 搜索信息列表接口。type: 0联想、1单曲、100歌手、1002用户、1014MV、1006歌词
    def search(self, keyWord, type=0, offset=0, limit=99):
        url = None
        paramJson = None
        if type == 0:  # 联想搜索
            url = "https://music.163.com/weapi/search/suggest/web"
            paramJson = "{\"s\": \"" + keyWord + "\", \"limit\": \"8\", \"csrf_token\": \"\"}"
        else:  # 云搜索-只搜歌曲
            url = "https://music.163.com/weapi/cloudsearch/get/web"
            paramJson = "{\"s\": \"" + str(keyWord) + "\", \"type\": \"" + str(type) + "\", \"offset\": \"" + str(
                offset) + "\", \"limit\": \"" + str(limit) + "\", \"total\":\"true\", \"csrf_token\": \"\"}"

        # 加密处理
        params, encSecKey = self.__getParamsAndEncSecKey(paramJson)

        res = self.__sendPost(url, params, encSecKey)
        if res:
            responseJson = json.loads(res)
            if responseJson['code'] == 200:
                result = responseJson['result']
                if type == 0:  # 联想搜索
                    songs = result.get('songs') # 单曲
                    mvs = result.get('mvs') # 视频
                    singer = result.get('artists') # 歌手
                    albums = result.get('albums') # 专辑
                    playlists = result.get('playlists') # 歌单
                    msg = ""
                    if songs :
                        msg += "歌单列表:\n"
                        for item in songs:
                            msg += "id【{}】\t名称【{}】- 歌手:{}【{}】\n".format(item['id'], item['name'], item['artists'][0]['name'], item['artists'][0]['id'])
                    if mvs :
                        msg += "视频列表:\n"
                        for item in mvs:
                            msg += "id【{}】\t名称【{}】- 歌手:{}【{}】- 播放次数：{} \n".format(item['id'], item['name'], item['artistName'], item['artistId'], item['playCount'])
                    if singer:
                        msg += "歌手列表:\n"
                        for item in singer: # artist
                            msg += "id【{}】\t名称【{}】- accountId:【{}】\n".format(item['id'], item['name'], item['accountId'])
                    if albums:
                        msg += "专辑列表:\n"
                        for item in albums:
                            msg += "id【{}】\t名称【{}】- 歌手:{}【{}】\n".format(item['id'], item['name'], item['artist']['name'], item['artist']['id'])
                    if playlists:
                        msg += "歌单列表:\n"
                        for item in playlists:
                            msg += "id【{}】\t名称【{}】- 播放次数：{} \n".format(item['id'], item['name'], item['playCount'])
                    print(msg)
                else:  # 搜索歌单
                    for item in result['songs']:
                        singer = item['ar'][0]
                        playList = item['al']
                        print("id【{}】\t名称【{}】- 歌手:{}【{}】专辑【{}】时长：{}秒 - 创建时间：{}".format(item['id'], item['name'], singer['name'], singer['id'], playList['name'], (item['dt'] / 1000), self.getTime(item['publishTime'] / 1000)))
                return result
            else:
                print("接口返回异常！code : {}, msg : {}".format(responseJson['code'], responseJson))
        else:
            print("接口挂了！！！")

        return None

    # 获取歌曲详细信息
    def getSongInfoBySongId(self, songId):
        # 获取歌曲信息api
        res = self.__sendGet('https://music.163.com/api/v3/song/detail?id=%s&c=[{\"id\":%%s}]' % songId % songId)
        if res:
            json_res = json.loads(res)
            # print("getSongInfoBySongId json_res = %s" % json_res)
            if json_res and json_res['code'] == 200:
                return json_res
        print("---- 接口请求异常 ----")
        return None

    # 获取歌曲下载连接
    def target_songs_downloadlink(self, songId):
        # 获取歌曲详细信息
        json = self.getSongInfoBySongId(songId)

        if json is not None and json["code"] == 200:
            songs_info = json['songs'][0]
            privileges_info = json['privileges'][0]

            # 获取歌曲不同音质的播放链接
            urls = {}
            self.getSongsPlayerUrl(songId, privileges_info['maxbr'], urls)
            if 'h' in songs_info:
                self.getSongsPlayerUrl(songId, songs_info['h']['br'], urls)
            if 'm' in songs_info:
                self.getSongsPlayerUrl(songId, songs_info['m']['br'], urls)
            if 'l' in songs_info:
                self.getSongsPlayerUrl(songId, songs_info['l']['br'], urls)
            if (len(urls) == 0):
                print("无法获取下载连接.")
                return

            # 获取歌曲信息
            singerNames = ''
            for item in songs_info['ar']:
                singerNames += item['name'] + '/'
            songName = songs_info["name"]  # 歌曲名称
            singerNames = singerNames[:len(singerNames) - 1]  # 歌手名称
            albumName = songs_info['al']['name']  # 专辑名称
            print("单曲下载: <{}> \t 歌手:{} \t 专辑:{} ".format(songName, singerNames, albumName))

            # 打印URL
            print("下载音质   \t 音质 ")
            for item in urls:
                print("url(音质) : \t {} ".format(item))

            # 选择下载链接的br
            print("#请选择(输入音质),返回(b)", end="\n>>")
            value = self.__input()
            if value == 'b':
                return
            url = urls[int(value)]

            # 歌曲命名 + 默认目录
            file_name = songName + '-(' + value + ').mp3'
            path = '/Users/kunda-liang/Desktop/music'

            # 设置下载歌曲的位置
            print("#指定下载目录, 直接回车使用默认目录[{}] , 返回(b)".format(path), end="\n>>")
            value = self.__input()
            if value == 'b':
                return
            if len(value) > 0:
                path = value
            if not os.path.exists(path):
                os.mkdir(path)

            self.__downloadTask(path, file_name, url)
        else:
            print('获取服务器信息失败.')

    # 获取单曲的播放链接 brs：音质
    def getSongsPlayerUrl(self, songId, brs, urlArr):
        url = 'https://music.163.com/weapi/song/enhance/player/url?csrf_token='
        paramJson = "{\"ids\": \"[" + str(songId) + "]\",\"br\": \"" + str(brs) + "\",\"csrf_token\": \"\"}"
        # 加密处理
        params, encSecKey = self.__getParamsAndEncSecKey(paramJson)

        res = self.__sendPost(url, params, encSecKey)
        # print("res = %s" %res)
        if res:
            responseJson = json.loads(res)
            if responseJson is not None and responseJson["code"] == 200:
                newBr = responseJson['data'][0]['br']  # 这里应该和brs是一致的
                downloadUrl = responseJson['data'][0]['url']
                if newBr not in urlArr and newBr > 0:
                    urlArr[newBr] = downloadUrl

    # 过滤规则取自github上其它人写的
    def __string_filter(self, str_v):
        # r1 = u'[’!"#$%&\'*+,-./:;<=>?@，。?★、…【】《》？“”‘’！[\\]^`{|}~]+'
        r1 = u'[#?*:"<>|，。?★、…【】《》？“”‘’！]+'
        r2 = u'[\\\\\/]+'
        # r2 = u'\s+'
        str_v = re.sub(r1, ' ', str_v)  # 过滤内容中的各种标点符号
        str_v = re.sub(r2, '_', str_v)  # 过滤内容中的各种标点符号
        # str_v = re.sub(r2, '_', str_v)  # 过滤内容中的各种标点符号
        return str_v.rstrip()

    # 下载任务
    def __downloadTask(self, path, file_name, url, chunk_size=1024):
        path = path.replace('\\', '/')
        file_name = self.__string_filter(file_name)
        if not path.endswith('/'):
            path = path + '/'
        if not os.path.exists(path):
            os.mkdir(path)
        print("连接数据中,请等待...")
        with closing(requests.get(url, stream=True)) as response:
            content_size = int(response.headers['content-length'])  # 内容体总大小
            print("总内容大小：%d B %d MB" % (content_size, content_size / 1000000))
            with open(path + file_name, "wb") as file:
                for data in response.iter_content(chunk_size=chunk_size):
                    file.write(data)
                    print('下载中，请稍后...')

            print('下载完成')

    # 加载 JS 程序
    def __readJSFile(self):
        # 只读的方式读取文件，且自动关闭资源
        with open("../script/my_core.js", 'r', encoding='UTF-8') as f:
            line = f.readline()
            htmlStr = ''
            while line:
                htmlStr += line
                line = f.readline()
        return htmlStr

    # 获取 JS 加密信息
    def __getParamsAndEncSecKey(self, paramsJson):
        jsStr = self.__readJSFile()
        ctx = execjs.compile(jsStr)  # 编译解析
        callResult = ctx.call('MyF', paramsJson)  # 调用JS函数
        print("params: %s" % callResult['encText'])
        print("encSecKey: %s" % callResult['encSecKey'])
        return callResult['encText'], callResult['encSecKey']

    # get 请求
    def __sendGet(self, url, header=base_header, count=3):
        response = None
        try:
            response = requests.get(url, headers=header)
            if response and response.status_code == 200:
                return response.content
            raise Exception(response.status_code)
        except Exception as e:
            print("出现异常：%s" % e)
            print('请求重试 code : %d' % (response.status_code if response else -1))
            if count > 0:
                time.sleep(1)  # 睡眠一秒
                count -= 1
                return self.__sendGet(url, header, count)
            else:
                return None
        finally:
            if response:
                response.close()

    # post 请求
    def __sendPost(self, url, params, encSecKey, header=base_header, count=3):
        response = None
        try:
            data = {
                "params": params,
                "encSecKey": encSecKey
            }
            response = requests.post(url, data=data, headers=header)
            if response and response.status_code == 200:
                return response.content
            raise Exception(response.status_code)
        except Exception as e:
            print("出现异常：%s" % e)
            print('请求重试 code : %d' % (response.status_code if response else -1))
            if count > 0:
                time.sleep(1)  # 睡眠一秒
                count -= 1
                return self.__sendPost(url, params, encSecKey, header, count)
            else:
                return None
        finally:
            if response:
                response.close()

    # 时间戳 -> 年月日
    def getTime(self, mytime):
        return time.strftime("%Y年%m月%d日 %H:%M:%S", time.localtime(mytime))

    # 等待控制台输入
    def __input(self):
        return stdin.readline().strip('\n')

    # 知乎大神破解的加密方式 Python 实现(2018-07-03已经不好用了) start
    # first_param = "{\"uid\":\"469833958\",\"type\":\"-1\",\"limit\":\"1000\",\"offset\":\"0\",\"total\":\"true\",\"csrf_token\":\"\"}"
    # second_param = "010001"
    # third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
    # forth_param = "0CoJUm6Qyw8W8jud"
    # encSecKey_param = "aec2b1c26af0b603531f164afae417b8e3cebfe231a6820f3fc86c9d0ddeafd5a4a83ac9baddf858f74ac15de85965ee2f18e9a91dbc0cae0eb4f0c8c75418ed4209c5775b480cc3be0a831797c5738678ced4c2935c3a2669f1f5517c7af84d69c0207fe77cf6936cf0bb70fe000bb93c468aea42f9e2ba0edb63a8567ec3f4"
    #
    # def __getParams(self):
    #     iv = "0102030405060708"
    #     first_key = forth_param
    #     second_key = 16 * 'F'
    #     h_encText = self.AES_encrypt(first_param, first_key, iv)
    #     h_encText = self.AES_encrypt(h_encText, second_key, iv)
    #     return h_encText
    #
    # def __getEncSecKey(self):
    #     encSecKey = encSecKey_param
    #     return encSecKey
    #
    # def __AES_encrypt(self, text, key, iv):
    #     pad = 16 - len(text) % 16
    #     text = text + pad * chr(pad)
    #
    #     encryptor = AES.new(key, AES.MODE_CBC, iv)
    #     encrypt_text = encryptor.encrypt(text)
    #     encrypt_text = base64.b64encode(encrypt_text)
    #
    #     return encrypt_text.decode()
    # 之乎大神破解的加密方式 Python 实现(2018-07-03已经不好用了) end


if __name__ == "__main__":
    api = Music_API()
    # 获取歌曲评论 432506345 开启热评后，页码和数量都会失效
    # api.getCommentsBySongId("432506345", 1, 10, False)

    # 获取用户top100的歌曲 469833958
    # api.getSongsByUser("469833958")

    # 运行JS文件 获取加密信息
    # api.__getParamsAndEncSecKey()

    # 获取用户歌单信息
    # api.getPlayListByUser("469833958")

    # 获取用户基本信息
    # api.getUserInfo("469833958")

    # 搜索信息（联想搜索)）
    # api.search("信仰")

    # 搜索歌曲信息
    # api.search("天后", 1)

    # 获取歌曲详细信息
    # api.getSongInfoBySongId("432506345")

    # 歌曲下载
    api.target_songs_downloadlink("434550872")
